一步一步DIY zepto库,研究zepto源码8--touch模块

由于移动端众所周知的click 300ms延迟的缘故(用户碰触页面之后,需要等待一段时间来判断是不是双击(double tap)动作,而不是立即响应单击(click),等待的这段时间大约是300ms)。移动事件提供了touchstarttouchmovetouchend,却没有提供对tap的支持。许多主流框架都是自定义实现了tap事件,消除300ms的延迟,当然包括Zepto.js。

关于点击穿透的解决方案可以查看: 移动页面点击穿透问题解决方案

此外,使用原生的touch事件也存在点击穿透的问题,因为click是在touch系列事件发生后大约300ms才触发的,混用touch和click肯定会导致点透问题。所以在移动端我们有必要使用类似Zepto.js的tap事件。

代码挂在我的github上,对应文件夹v0.8.1。
https://github.com/zrysmt/DIY-zepto

1.源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

var Touch = function($) {
var touch = {},
touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
longTapDelay = 750,
gesture;
// 判断滑动方向,返回Left, Right, Up, Down
function swipeDirection(x1, x2, y1, y2) {
return Math.abs(x1 - x2) >=
Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down');
}
//长按
function longTap() {
longTapTimeout = null;
if (touch.last) {
touch.el.trigger('longTap');
touch = {};
}
}
//取消长按
function cancelLongTap() {
if (longTapTimeout) clearTimeout(longTapTimeout);
longTapTimeout = null;
}
//取消所有
function cancelAll() {
if (touchTimeout) clearTimeout(touchTimeout);
if (tapTimeout) clearTimeout(tapTimeout);
if (swipeTimeout) clearTimeout(swipeTimeout);
if (longTapTimeout) clearTimeout(longTapTimeout);
touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null;
touch = {};
}
// IE的touch事件
function isPrimaryTouch(event) {
return (event.pointerType == 'touch' ||
event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary
}
// IE鼠标事件
function isPointerEventType(e, type) {
return (e.type == 'pointer' + type ||
e.type.toLowerCase() == 'mspointer' + type)
}

$(document).ready(function() {
var now, delta, deltaX = 0,
deltaY = 0,
firstTouch, _isPointerType;
//IE的手势
if ('MSGesture' in window) {
gesture = new MSGesture();
gesture.target = document.body;
}
$(document)
.bind('MSGestureEnd', function(e) { //处理IE手势结束
var swipeDirectionFromVelocity =
e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null;
if (swipeDirectionFromVelocity) {
touch.el.trigger('swipe');
touch.el.trigger('swipe' + swipeDirectionFromVelocity);
}
})
// 处理手指接触
.on('touchstart MSPointerDown pointerdown', function(e) {
//排除非触摸设备
if ((_isPointerType = isPointerEventType(e, 'down')) &&
!isPrimaryTouch(e)) return;
firstTouch = _isPointerType ? e : e.touches[0]; // 获取起点位置数据
// 重置终点坐标
if (e.touches && e.touches.length === 1 && touch.x2) {
// Clear out touch movement data if we have it sticking around
// This can occur if touchcancel doesn't fire due to preventDefault, etc.
touch.x2 = undefined;
touch.y2 = undefined;
}
// 判断用户动作类型
now = Date.now();
delta = now - (touch.last || now); // 距离上次碰触的时间差
touch.el = $('tagName' in firstTouch.target ?
firstTouch.target : firstTouch.target.parentNode); // 手指碰触的元素
touchTimeout && clearTimeout(touchTimeout); // 重置touch事件处理器的Timeout ID
//记录起点坐标
touch.x1 = firstTouch.pageX;
touch.y1 = firstTouch.pageY;
//判断是否双击
if (delta > 0 && delta <= 250) touch.isDoubleTap = true;
touch.last = now;
// 注册长按事件处理器ID
longTapTimeout = setTimeout(longTap, longTapDelay);
// adds the current touch contact for IE gesture recognition
// 支持IE手势识别
if (gesture && _isPointerType) gesture.addPointer(e.pointerId);
})
// 处理手指滑动
.on('touchmove MSPointerMove pointermove', function(e) {
// 排除非触摸设备
if ((_isPointerType = isPointerEventType(e, 'move')) &&
!isPrimaryTouch(e)) return;
firstTouch = _isPointerType ? e : e.touches[0];
cancelLongTap(); // 取消长按事件处理器
touch.x2 = firstTouch.pageX;
touch.y2 = firstTouch.pageY;

deltaX += Math.abs(touch.x1 - touch.x2);
deltaY += Math.abs(touch.y1 - touch.y2);
})
// 处理手指离开
.on('touchend MSPointerUp pointerup', function(e) {
// 排除非触摸设备
if ((_isPointerType = isPointerEventType(e, 'up')) &&
!isPrimaryTouch(e)) return;
cancelLongTap(); // 取消长按事件处理器

// swipe 判定滑动动作(起点 - 终点的横向或者纵向距离超过30px)
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) {


// 注册长按事件处理器ID(立即准备执行长按)
swipeTimeout = setTimeout(function() {
if (touch.el) {
touch.el.trigger('swipe'); // 触发长按
// 触发向上|下|左|右的长按
touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
}
touch = {}; // 清空数据,本次touch结束
}, 0);
}
// normal tap 正常轻触
else if ('last' in touch) { // 如果记录了上次接触时间
// don't fire tap when delta position changed by more than 30 pixels,
// for instance when moving to a point and back to origin
if (deltaX < 30 && deltaY < 30) {
// delay by one tick so we can cancel the 'tap' event if 'scroll' fires
// ('tap' fires before 'scroll')
//立即准备执行轻触,不立即执行是为了scroll时能取消执行轻触
tapTimeout = setTimeout(function() {
// trigger universal 'tap' with the option to cancelTouch()
// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
// 触发全局tap,cancelTouch()可以取消singleTap,doubleTap事件,以求更快响应轻触
var event = $.Event('tap');
event.cancelTouch = cancelAll;
// [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
if (touch.el) touch.el.trigger(event);

// trigger double tap immediately
// 立即触发doubleTap
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap');
touch = {};
}

// trigger single tap after 250ms of inactivity
// 250ms后触发singleTap
else {
touchTimeout = setTimeout(function() {
touchTimeout = null;
if (touch.el) touch.el.trigger('singleTap');
touch = {};
}, 250);
}
}, 0);
} else { // 如果是滑了一圈又回到起点,扔掉事件数据,不做处理
touch = {};
}
deltaX = deltaY = 0; // 重置横向,纵向滑动距离
}
})
// when the browser window loses focus,
// for example when a modal dialog is shown,
// cancel all ongoing events
// 浏览器窗口失去焦点时,取消所有事件处理动作
.on('touchcancel MSPointerCancel pointercancel', cancelAll);

// scrolling the window indicates intention of the user
// to scroll, not tap or swipe, so cancel all ongoing events
// 触发scroll时取消所有事件处理动作
$(window).on('scroll', cancelAll);
});

//在这里注册,在源码中触发
['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
'doubleTap', 'tap', 'singleTap', 'longTap'
].forEach(function(eventName) {
$.fn[eventName] = function(callback) {
return this.on(eventName, callback);
};
});

};

export default Touch;

2.源码分析

核心层分是把事件绑定到$(document)上分别进行处理

1
2
3
4
5
6
7
$(document)
.bind('MSGestureEnd', function(e){}//处理IE手势结束
.on('touchstart MSPointerDown pointerdown', function(e) {} // 处理手指接触
.on('touchmove MSPointerMove pointermove', function(e) {} // 处理手指滑动
.on('touchend MSPointerUp pointerup', function(e) {} // 处理手指离开
.on('touchcancel MSPointerCancel pointercancel', cancelAll);
// 浏览器窗口失去焦点时,取消所有事件处理动作
1
2
// 触发scroll时取消所有事件处理动作
$(window).on('scroll', cancelAll);

tap事件是通过touch事件模拟的

  • tap —元素tap的时候触发。
  • singleTap and doubleTap — 这一对事件可以用来检测元素上的单击和双击。(如果你不需要检测单击、双击,使用 tap 代替)。
  • longTap — 当一个元素被按住超过750ms触发。
  • swipe, swipeLeft, swipeRight, swipeUp, swipeDown — 当元素被划过时触发。(可选择给定的方向)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (deltaX < 30 && deltaY < 30) {
//立即准备执行轻触,不立即执行是为了scroll时能取消执行轻触
tapTimeout = setTimeout(function() {
// 触发全局tap,cancelTouch()可以取消singleTap
// doubleTap事件,以求更快响应轻触
var event = $.Event('tap');
event.cancelTouch = cancelAll;
if (touch.el) touch.el.trigger(event);
// 立即触发doubleTap
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap');
touch = {};
}
// 250ms后触发singleTap
else {
touchTimeout = setTimeout(function() {
touchTimeout = null;
if (touch.el) touch.el.trigger('singleTap');
touch = {};
}, 250);
}
}, 0);
} else { // 如果是滑了一圈又回到起点,扔掉事件数据,不做处理
touch = {};
}

Zepto的touch模块也只实现了tap和swipe相关的动作,不支持复杂手势,需要支持复杂手势的话,可以使用 hammer.js ,hammer提供了完善的一整套手势支持( 注意 :hammer也存在点击穿透问题,仍然需要手动处理该问题)

代码挂在我的github上,对应文件夹v0.8.1。
https://github.com/zrysmt/DIY-zepto

参考阅读: